/*
 *	JorbisFormatConversionProvider.java
 *
 *	This file is part of Tritonus: http://www.tritonus.org/
 */

/*
 *  Copyright (c) 1999 - 2003 by Matthias Pfisterer
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 |<---            this code is formatted to fit into 80 columns             --->|
 */

package org.tritonus.sampled.convert.jorbis;

import java.io.EOFException;
import java.io.InputStream;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

import org.tritonus.share.TDebug;
import org.tritonus.share.sampled.AudioFormats;
import org.tritonus.share.sampled.convert.TAsynchronousFilteredAudioInputStream;
import org.tritonus.share.sampled.convert.TEncodingFormatConversionProvider;

import com.jcraft.jogg.SyncState;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.Packet;
import com.jcraft.jorbis.Info;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.DspState;
import com.jcraft.jorbis.Block;

/**
 * Pure-java decoder for ogg vorbis streams. The FormatConversionProvider uses
 * the pure-java ogg vorbis decoder from www.jcraft.com/jorbis/.
 * <p>
 * See vorbis spec for more info: http://xiph.org/vorbis/doc/Vorbis_I_spec.html
 * 
 * @author Matthias Pfisterer
 */
public class JorbisFormatConversionProvider extends TEncodingFormatConversionProvider {
	// only used as abbreviation
	private static final AudioFormat.Encoding VORBIS = new AudioFormat.Encoding("VORBIS");
	private static final AudioFormat.Encoding PCM_SIGNED = new AudioFormat.Encoding("PCM_SIGNED");

	private static final AudioFormat[] INPUT_FORMATS = {
			// mono
			// TODO: mechanism to make the double specification with
			// different endianess obsolete.
			new AudioFormat(VORBIS, -1.0F, -1, 1, -1, -1.0F, false), new AudioFormat(VORBIS, -1.0F, -1, 1, -1, -1.0F, true),
			// stereo
			new AudioFormat(VORBIS, -1.0F, -1, 2, -1, -1.0F, false), new AudioFormat(VORBIS, -1.0F, -1, 2, -1, -1.0F, true),
	// TODO: other channel configurations
	};

	private static final AudioFormat[] OUTPUT_FORMATS = {
			// mono, 16 bit signed
			new AudioFormat(PCM_SIGNED, -1.0F, 16, 1, 2, -1.0F, false), new AudioFormat(PCM_SIGNED, -1.0F, 16, 1, 2, -1.0F, true),
			// stereo, 16 bit signed
			new AudioFormat(PCM_SIGNED, -1.0F, 16, 2, 4, -1.0F, false), new AudioFormat(PCM_SIGNED, -1.0F, 16, 2, 4, -1.0F, true),
	// TODO: other channel configurations
	};

	/**
	 * Constructor.
	 */
	// TODO: check interaction with base class
	public JorbisFormatConversionProvider() {
		super(Arrays.asList(INPUT_FORMATS), Arrays.asList(OUTPUT_FORMATS)/*
																		 * ,
																		 * true,
																		 * //
																		 * new
																		 * behaviour
																		 * false
																		 */); // bidirectional
																				// ..
																				// constants
																				// UNIDIR../BIDIR..?
	}

	public AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream audioInputStream) {
		/**
		 * The AudioInputStream to return.
		 */
		AudioInputStream convertedAudioInputStream = null;

		if (TDebug.TraceAudioConverter) {
			TDebug.out(">JorbisFormatConversionProvider.getAudioInputStream(): begin");
			TDebug.out("checking if conversion supported");
			TDebug.out("from: " + audioInputStream.getFormat());
			TDebug.out("to: " + targetFormat);
		}

		// what is this ???
		targetFormat = getDefaultTargetFormat(targetFormat, audioInputStream.getFormat());
		if (isConversionSupported(targetFormat, audioInputStream.getFormat())) {
			if (TDebug.TraceAudioConverter) {
				TDebug.out("conversion supported; trying to create DecodedJorbisAudioInputStream");
			}
			convertedAudioInputStream = new DecodedJorbisAudioInputStream(targetFormat, audioInputStream);
		} else {
			if (TDebug.TraceAudioConverter) {
				TDebug.out("conversion not supported; throwing IllegalArgumentException");
				TDebug.out("<");
			}
			throw new IllegalArgumentException("conversion not supported");
		}
		if (TDebug.TraceAudioConverter) {
			TDebug.out("<JorbisFormatConversionProvider.getAudioInputStream(): end");
		}
		return convertedAudioInputStream;
	}

	// TODO: recheck !!
	protected AudioFormat getDefaultTargetFormat(AudioFormat targetFormat, AudioFormat sourceFormat) {
		if (TDebug.TraceAudioConverter) {
			TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): target format: " + targetFormat);
		}
		if (TDebug.TraceAudioConverter) {
			TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): source format: " + sourceFormat);
		}
		AudioFormat newTargetFormat = null;
		// return first of the matching formats
		// pre-condition: the predefined target formats (FORMATS2) must be
		// well-defined !
		Iterator iterator = getCollectionTargetFormats().iterator();
		while (iterator.hasNext()) {
			AudioFormat format = (AudioFormat) iterator.next();
			if (AudioFormats.matches(targetFormat, format)) {
				newTargetFormat = format;
			}
		}
		if (newTargetFormat == null) {
			throw new IllegalArgumentException("conversion not supported");
		}
		if (TDebug.TraceAudioConverter) {
			TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): new target format: " + newTargetFormat);
		}
		// hacked together...
		// ... only works for PCM target encoding ...
		newTargetFormat = new AudioFormat(targetFormat.getEncoding(), sourceFormat.getSampleRate(), newTargetFormat.getSampleSizeInBits(), newTargetFormat.getChannels(),
				newTargetFormat.getFrameSize(), sourceFormat.getSampleRate(), newTargetFormat.isBigEndian());
		if (TDebug.TraceAudioConverter) {
			TDebug.out("JorbisFormatConversionProvider.getDefaultTargetFormat(): really new target format: " + newTargetFormat);
		}
		return newTargetFormat;
	}

	/**
	 * AudioInputStream returned on decoding of ogg vorbis. An instance of this
	 * class is returned if you call
	 * AudioSystem.getAudioInputStream(AudioFormat, AudioInputStream) to decode
	 * an ogg/vorbis stream. This class contains the logic of maintaining
	 * buffers and calling the decoder.
	 */
	/*
	 * Class should be private, but is public due to a bug (?) in the aspectj
	 * compiler.
	 */
	/* private */public static class DecodedJorbisAudioInputStream extends TAsynchronousFilteredAudioInputStream {
		private static final int BUFFER_MULTIPLE = 4;
		private static final int BUFFER_SIZE = BUFFER_MULTIPLE * 256 * 2;
		private static final int CONVSIZE = BUFFER_SIZE * 2;

		private InputStream m_oggBitStream = null;

		// Ogg structures
		private SyncState m_oggSyncState = null;
		private StreamState m_oggStreamState = null;
		private Page m_oggPage = null;
		private Packet m_oggPacket = null;

		// Vorbis structures
		private Info m_vorbisInfo = null;
		private Comment m_vorbisComment = null;
		private DspState m_vorbisDspState = null;
		// actually is an ogg structure
		private Block m_vorbisBlock = null;

		private List<String> m_songComments = new ArrayList<String>();
		// is altered later in a dubious way
		private int convsize = -1; // BUFFER_SIZE * 2;
		// TODO: further checking
		private byte[] convbuffer = new byte[CONVSIZE];
		private float[][][] _pcmf = null;
		private int[] _index = null;

		// TODO: introduce state variable
		private boolean m_bHeadersExpected;

		/**
		 * Constructor.
		 */
		public DecodedJorbisAudioInputStream(AudioFormat outputFormat, AudioInputStream bitStream) {
			super(outputFormat, AudioSystem.NOT_SPECIFIED);
			if (TDebug.TraceAudioConverter) {
				TDebug.out("DecodedJorbisAudioInputStream.<init>(): begin");
			}
			m_oggBitStream = bitStream;
			m_bHeadersExpected = true;
			init_jorbis();
			if (TDebug.TraceAudioConverter) {
				TDebug.out("DecodedJorbisAudioInputStream.<init>(): end");
			}
		}

		/**
		 * Initializes all the jOrbis and jOgg vars that are used for song
		 * playback.
		 */
		private void init_jorbis() {
			m_oggSyncState = new SyncState();
			m_oggStreamState = new StreamState();
			m_oggPage = new Page();
			m_oggPacket = new Packet();

			m_vorbisInfo = new Info();
			m_vorbisComment = new Comment();
			m_vorbisDspState = new DspState();
			m_vorbisBlock = new Block(m_vorbisDspState);

			m_oggSyncState.init();
		}

		/**
		 * Callback from circular buffer.
		 */
		public void execute() {
			if (TDebug.TraceAudioConverter)
				TDebug.out(">DecodedJorbisAudioInputStream.execute(): begin");
			if (m_bHeadersExpected) {
				if (TDebug.TraceAudioConverter)
					TDebug.out("reading headers...");
				// Headers (+ Comments).
				try {
					readHeaders();
				} catch (IOException e) {
					if (TDebug.TraceAllExceptions) {
						TDebug.out(e);
					}
					closePhysicalStream();
					if (TDebug.TraceAudioConverter)
						TDebug.out("<DecodedJorbisAudioInputStream.execute(): end");
					return;
				}
				m_bHeadersExpected = false;
				setupVorbisStructures();
			}
			if (TDebug.TraceAudioConverter)
				TDebug.out("decoding...");
			// Decoding !
			while (writeMore()) {
				try {
					readOggPacket();
				} catch (IOException e) {
					if (TDebug.TraceAllExceptions) {
						TDebug.out(e);
					}
					closePhysicalStream();
					if (TDebug.TraceAudioConverter)
						TDebug.out("<DecodedJorbisAudioInputStream.execute(): end");
					return;
				}
				decodeDataPacket();
			}
			if (m_oggPacket.e_o_s != 0) {
				if (TDebug.TraceAudioConverter)
					TDebug.out("end of vorbis stream reached");
				shutDownLogicalStream();
			}
			if (TDebug.TraceAudioConverter)
				TDebug.out("<DecodedJorbisAudioInputStream.execute(): end");
		}

		/*
		 * The end of the vorbis stream is reached. So we shut down the logical
		 * bitstream and vorbis structures.
		 */
		private void shutDownLogicalStream() {
			m_oggStreamState.clear();
			m_vorbisBlock.clear();
			m_vorbisDspState.clear();
			m_vorbisInfo.clear();
			m_bHeadersExpected = true;
		}

		private void closePhysicalStream() {
			if (TDebug.TraceAudioConverter)
				TDebug.out("DecodedJorbisAudioInputStream.closePhysicalStream(): begin");
			m_oggSyncState.clear();
			try {
				if (m_oggBitStream != null) {
					m_oggBitStream.close();
				}
				getCircularBuffer().close();
			} catch (Exception e) {
				if (TDebug.TraceAllExceptions) {
					TDebug.out(e);
				}
			}
			if (TDebug.TraceAudioConverter)
				TDebug.out("DecodedJorbisAudioInputStream.closePhysicalStream(): end");
		}

		/**
		 * Read and process all three vorbis headers.
		 */
		private void readHeaders() throws IOException {
			readIdentificationHeader();
			readCommentAndCodebookHeaders();
			processComments();
		}

		/**
		 * Read the vorbis identification header.
		 * 
		 * @throw IOException
		 */
		private void readIdentificationHeader() throws IOException {
			readOggPage();
			m_oggStreamState.init(m_oggPage.serialno());
			m_vorbisInfo.init();
			m_vorbisComment.init();
			if (m_oggStreamState.pagein(m_oggPage) < 0) {
				throw new IOException("can't read first page of Ogg bitstream data, perhaps stream version mismatch");
			}
			if (m_oggStreamState.packetout(m_oggPacket) != 1) {
				throw new IOException("can't read initial header packet");
			}
			if (m_vorbisInfo.synthesis_headerin(m_vorbisComment, m_oggPacket) < 0) {
				throw new IOException("packet is not a vorbis header");
			}
		}

		/**
		 * Read the comment header and the codebook header pages.
		 */
		private void readCommentAndCodebookHeaders() throws IOException {
			for (int i = 0; i < 2; i++) {
				readOggPacket();
				if (m_vorbisInfo.synthesis_headerin(m_vorbisComment, m_oggPacket) < 0) {
					throw new IOException("packet is not a vorbis header");
				}
			}
		}

		/**
		 */
		private void processComments() {
			byte[][] ptr = m_vorbisComment.user_comments;
			String currComment = "";
			m_songComments.clear();
			for (int j = 0; j < ptr.length; j++) {
				if (ptr[j] == null) {
					break;
				}
				currComment = (new String(ptr[j], 0, ptr[j].length - 1)).trim();
				m_songComments.add(currComment);
				/*
				 * if (currComment.toUpperCase().startsWith("ARTIST")) { String
				 * artistLabelValue = currComment.substring(7); } else if
				 * (currComment.toUpperCase().startsWith("TITLE")) { String
				 * titleLabelValue = currComment.substring(6); String
				 * miniDragLabel = currComment.substring(6); }
				 */
				if (TDebug.TraceAudioConverter)
					TDebug.out("Comment: " + currComment);
			}
			currComment = "Bitstream: " + m_vorbisInfo.channels + " channel," + m_vorbisInfo.rate + "Hz";
			m_songComments.add(currComment);
			if (TDebug.TraceAudioConverter)
				TDebug.out(currComment);
			if (TDebug.TraceAudioConverter)
				currComment = "Encoded by: " + new String(m_vorbisComment.vendor, 0, m_vorbisComment.vendor.length - 1);
			m_songComments.add(currComment);
			if (TDebug.TraceAudioConverter)
				TDebug.out(currComment);
		}

		/**
		 * Setup structures needed for vorbis decoding. Precondition:
		 * m_vorbisInfo has to be initialized completely (i.e. all three headers
		 * are read).
		 */
		private void setupVorbisStructures() {
			convsize = BUFFER_SIZE / m_vorbisInfo.channels;
			m_vorbisDspState.synthesis_init(m_vorbisInfo);
			m_vorbisBlock.init(m_vorbisDspState);
			_pcmf = new float[1][][];
			_index = new int[m_vorbisInfo.channels];
		}

		/**
		 * Decode a packet of vorbis data. This method assumes that a packet is
		 * available in {@link #m_oggPacket m_oggPacket}. The content of this
		 * packet is run through the decoder. The resulting PCM data are written
		 * to the circular buffer.
		 */
		private void decodeDataPacket() {
			int samples;
			if (m_vorbisBlock.synthesis(m_oggPacket) == 0) { // test for
																// success!
				m_vorbisDspState.synthesis_blockin(m_vorbisBlock);
			}
			while ((samples = m_vorbisDspState.synthesis_pcmout(_pcmf, _index)) > 0) {
				float[][] pcmf = _pcmf[0];
				int bout = (samples < convsize ? samples : convsize);
				// convert floats to signed ints and
				// interleave
				for (int nChannel = 0; nChannel < m_vorbisInfo.channels; nChannel++) {
					int pointer = nChannel * getSampleSizeInBytes();
					int mono = _index[nChannel];
					for (int j = 0; j < bout; j++) {
						float fVal = pcmf[nChannel][mono + j];
						clipAndWriteSample(fVal, pointer);
						pointer += getFrameSize();
					}
				}
				m_vorbisDspState.synthesis_read(bout);
				getCircularBuffer().write(convbuffer, 0, getFrameSize() * bout);
			}
		}

		/**
		 * Scale and clip the sample and write it to convbuffer.
		 */
		private void clipAndWriteSample(float fSample, int nPointer) {
			int nSample;
			// TODO: check if clipping is necessary
			if (fSample > 1.0F) {
				fSample = 1.0F;
			}
			if (fSample < -1.0F) {
				fSample = -1.0F;
			}
			switch (getFormat().getSampleSizeInBits()) {
			case 16:
				nSample = (int) (fSample * 32767.0F);
				if (isBigEndian()) {
					convbuffer[nPointer++] = (byte) (nSample >> 8);
					convbuffer[nPointer] = (byte) (nSample & 0xFF);
				} else {
					convbuffer[nPointer++] = (byte) (nSample & 0xFF);
					convbuffer[nPointer] = (byte) (nSample >> 8);
				}
				break;

			case 24:
				nSample = (int) (fSample * 8388607.0F);
				if (isBigEndian()) {
					convbuffer[nPointer++] = (byte) (nSample >> 16);
					convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF);
					convbuffer[nPointer] = (byte) (nSample & 0xFF);
				} else {
					convbuffer[nPointer++] = (byte) (nSample & 0xFF);
					convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF);
					convbuffer[nPointer] = (byte) (nSample >> 16);
				}
				break;

			case 32:
				nSample = (int) (fSample * 2147483647.0F);
				if (isBigEndian()) {
					convbuffer[nPointer++] = (byte) (nSample >> 24);
					convbuffer[nPointer++] = (byte) ((nSample >>> 16) & 0xFF);
					convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF);
					convbuffer[nPointer] = (byte) (nSample & 0xFF);
				} else {
					convbuffer[nPointer++] = (byte) (nSample & 0xFF);
					convbuffer[nPointer++] = (byte) ((nSample >>> 8) & 0xFF);
					convbuffer[nPointer++] = (byte) ((nSample >>> 16) & 0xFF);
					convbuffer[nPointer] = (byte) (nSample >> 24);
				}
				break;
			}
		}

		/**
		 * Read an ogg packet. This method does everything necessary to read an
		 * ogg packet. If needed, it calls {@link #readOggPage readOggPage()},
		 * which, in turn, may read more data from the stream. The resulting
		 * packet is placed in {@link #m_oggPacket m_oggPacket} (for which the
		 * reference is not altered; is has to be initialized before).
		 */
		private void readOggPacket() throws IOException {
			while (true) {
				int result = m_oggStreamState.packetout(m_oggPacket);
				if (result == 1) {
					return;
				}
				if (result == -1) {
					throw new IOException("can't read packet");
				}
				readOggPage();
				if (m_oggStreamState.pagein(m_oggPage) < 0) {
					throw new IOException("can't read page of Ogg bitstream data");
				}
			}
		}

		/**
		 * Read an ogg page. This method does everything necessary to read an
		 * ogg page. If needed, it reads more data from the stream. The
		 * resulting page is placed in {@link #m_oggPage m_oggPage} (for which
		 * the reference is not altered; is has to be initialized before).
		 * 
		 * Note: this method doesn't deliver the page read to a StreamState
		 * object (which assembles pages to packets). This has to be done by the
		 * caller.
		 */
		private void readOggPage() throws IOException {
			while (true) {
				int result = m_oggSyncState.pageout(m_oggPage);
				if (result == 1) {
					return;
				}
				// we need more data from the stream
				int nIndex = m_oggSyncState.buffer(BUFFER_SIZE);
				// TODO: call stream.read() directly
				int nBytes = readFromStream(m_oggSyncState.data, nIndex, BUFFER_SIZE);
				// TODO: This clause should become obsolete; readFromStream()
				// should
				// propagate exceptions directly.
				if (nBytes == -1) {
					throw new EOFException();
				}
				m_oggSyncState.wrote(nBytes);
			}
		}

		/**
		 * Read raw data from to ogg bitstream. Reads from @ #m_oggBitStream
		 * m_oggBitStream} a specified number of bytes into a buffer, starting
		 * at a specified buffer index.
		 * 
		 * @param buffer
		 *            the where the read data should be put into. Its length has
		 *            to be at least nStart + nLength.
		 * @param nStart
		 * @param nLength
		 *            the number of bytes to read
		 * @return the number of bytes read (maybe 0) or -1 if there is no more
		 *         data in the stream.
		 */
		private int readFromStream(byte[] buffer, int nStart, int nLength) throws IOException {
			return m_oggBitStream.read(buffer, nStart, nLength);
		}

		/**
		 */
		private int getSampleSizeInBytes() {
			return getFormat().getFrameSize() / getFormat().getChannels();
		}

		/**
		 * .
		 * 
		 * @return .
		 */
		private int getFrameSize() {
			return getFormat().getFrameSize();
		}

		/**
		 * Returns if this stream (the decoded one) is big endian.
		 * 
		 * @return true if this stream is big endian.
		 */
		private boolean isBigEndian() {
			return getFormat().isBigEndian();
		}

		/**
		 *
		 */
		public void close() throws IOException {
			super.close();
			m_oggBitStream.close();
		}

	}
}

/*** JorbisFormatConversionProvider.java ***/
